Skip to content

fix(engine): fail on a null cwd, and route agent workdir filesystem checks through caps - #74

Merged
senamakel merged 11 commits into
tinyhumansai:mainfrom
senamakel:pr73-followup
Aug 24, 2026
Merged

fix(engine): fail on a null cwd, and route agent workdir filesystem checks through caps#74
senamakel merged 11 commits into
tinyhumansai:mainfrom
senamakel:pr73-followup

Conversation

@senamakel

Copy link
Copy Markdown
Member

Follow-up to #73, which merged before its review threads were worked. Four
threads, three addressed and one declined with evidence.

1. A cwd expression resolving to null was silently ignored (P1 — fixed)

Thread PRRT_kwDOTLXaD86bfFah, src/nodes/integration/agent_request.rs:200.

Confirmed against main. declared_working_dir read the key as
cfg.get(key).filter(|v| !v.is_null()), and cfg arrives already
expression-resolved
— so "cwd": "=nodes.prepare.item.json.worktree" whose
upstream path is missing became JSON null and read as absent. The loop then
fell through to working_dir, then to the agent definition's own directory,
then to whatever the harness defaults to. The step runs in a different checkout
and says nothing, which is exactly the failure the feature exists to remove.

A present-but-null value now fails the node, with a message that says why.
Regression tests: a_cwd_expression_that_resolves_to_null_fails_the_step and
a_null_cwd_does_not_fall_back_to_working_dir (the second pins the
cwdworking_dir fall-through specifically).

sub_workflow's config.workspace was checked for the same bug and does not
have it: its null filter runs on the raw config before expr::resolve, so an
expression resolving to null reaches .as_str() and already errors. Left alone.

2. Test filename policy (P1 — declined, --no-resolve)

Thread PRRT_kwDOTLXaD86bfFaj, tests/agent_workdir_e2e.rs:1.

AGENTS.md L6-L7 reads: "Keep Rust tests in files whose names end in
_tests.rs; do not add large inline test modules to production source files."

The second clause is what the rule is for, and the repo enforces it exactly
there: 127 of 127 test files under src/ end in _tests.rs, with no
exceptions.

tests/ follows a different, equally consistent convention — the integration
target is named for the scenario it drives:

  • 30 of 43 files end in _e2e.rs (data_flow_e2e.rs, error_recovery_e2e.rs,
    approval_async_e2e.rs, reliability_e2e.rs, hitl_e2e.rs, …)
  • 6 end in _tests.rs
  • the rest are fuzz_*.rs / smoke_all_nodes.rs / reference_workflows.rs

Renaming one file to agent_workdir_e2e_tests.rs would make it the only
integration target in the repo with that suffix and would not move the
directory any closer to consistent. If the maintainer wants tests/ covered by
the rule, that is a sweep of 37 files plus an AGENTS.md clarification, not a
change to this one file — happy to do it as its own PR. Left for a human to
call.

3. src/validate.rs over the 500-line limit (P1 — fixed)

Thread PRRT_kwDOTLXaD86bfFam. Confirmed: 598 lines against the AGENTS.md L3-L5
limit of 500.

Split at a real seam rather than at line 500. validate_all is two kinds of
check interleaved: graph shape (duplicate ids, trigger count, edge integrity,
void topology, condition routing, declared inputs) and per-kind node
config (sub_workflow child reference, per-item fan-out selectors, memory
scope, dedup key, approval enums). The config half never looks at an edge —
every check in it is one node in isolation — so it moved wholesale to
src/validate/node_config.rs beside the existing agents/loops/scatter/
workdir submodules.

src/validate.rs 598 → 277; src/validate/node_config.rs 344. Public surface
unchanged (validate, validate_all, unresolved_agent_refs), no test
changes needed.

4. Route the filesystem checks through a caps capability (Major — implemented)

Thread PRRT_kwDOTLXaD86bfIBo, src/workdir.rs:127.

The point holds. The engine's own shell node is the counter-example that
proves it: shell hands config.cwd to the ShellRunner untouched, and
containment happens inside the host's ScriptPolicy under caps/host/. The
agent path did the same job with std::fs::canonicalize in the engine, so an
AgentRunner owning a remote or containerized workspace had every cwd
rejected before its harness saw the request — and the workspace itself failed
first, at workspace.canonicalize(). The existing mitigation (a run with no
workspace resolves nothing) only covers a host that never pins one; it does
nothing for a host with a valid remote workspace, which is precisely the case
raised.

Implemented, split by what is actually an outside-world effect:

  • Shape — absolute vs relative, .. traversal — is string arithmetic with
    no filesystem in it. Factored out as workdir::check_shape and run first, on
    every host, always. Keeping it in the engine means a host implementation
    cannot accidentally drop the containment check that matters most.
  • Existence, canonical form, directory-ness now route through
    AgentRunner::resolve_workdir(workspace, declared) -> WorkdirCheck, with
    three answers: Resolved(path) (the host's canonical path, which it asserts
    is contained), Refused(reason) (fails the step with that reason, prefixed
    with the node), and Unmanaged.

Unmanaged is the default method body, so no existing AgentRunner changes
and no existing behaviour changes: the engine checks its own disk exactly as
before. That also avoids adding a field to Capabilities, which would have
broken all ~19 struct-literal construction sites and every downstream host. It
fits the trait's existing design — run, resolve_agent, resolve_context and
resolve_tools are all defaulted the same way, each degrading to the
pre-existing behaviour.

resolve_node_dir became async (a remote check is I/O); the three call sites
were already in async fns. sub_workflow's config.workspace goes through the
same seam.

Five new unit tests in src/workdir_tests.rs cover a harness-owned remote
workspace resolving, a harness refusal, Unmanaged falling back to the local
filesystem, and the shape check running before the harness is consulted (a
host cannot bless a .. escape).

Also

  • wiki/Node-Catalog.md "Where a step runs" documents the null rule and the
    filesystem split.
  • CHANGELOG.md — the working-directory entry is still under [Unreleased], so
    it was amended in place rather than given a Fixed section, and
    AgentRunner::resolve_workdir / caps::WorkdirCheck added.
  • Every touched file is under the 500-line limit (largest: node_config.rs at
    344, agent_request.rs at 475).

Commands run

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo test --all-features

All clean. Test counts, baseline → this branch:

before after
cargo test 1157 1161
cargo test --all-features 1401 (46 binaries) 1407 (46 binaries)

+4 unit tests (workdir_tests), +2 e2e (agent_workdir_e2e, behind mock).
No failures, no regressions.

senamakel and others added 11 commits August 23, 2026 15:28
…unction

Move the large block of per-node config validation logic from `validate_all` into a dedicated `validate_node_configs` function, reducing the main validation function by over 300 lines and improving readability. The extracted function handles sub-workflow, fan-out, memory, dedup, and approval node config checks, keeping the same validation behaviour.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Introduce validation for node configurations by adding a new module and importing its validation function, ensuring that node configs are checked alongside existing validations.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
…g back

A null value for `cwd` or `working_dir` now fails the node with a clear error, rather than being treated as if the key were absent. This prevents a step from silently running in a different directory when an expression resolves to null because the upstream node failed or a key moved.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add two end-to-end tests that verify the agent correctly fails a step when the `cwd` expression resolves to `null`, rather than silently falling back to the working directory or treating the value as absent.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Introduces a new public enum `WorkdirCheck` and a default method `resolve_workdir` on the `AgentRunner` trait, allowing hosts to resolve and validate a node's declared working directory against their own filesystem. The engine previously had no way to check path existence or containment on a remote or sandboxed filesystem, so this change delegates that responsibility to the host while preserving backward compatibility through a default implementation that returns `Unmanaged`.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The `WorkdirCheck` type was added to the agent module but not re-exported from the caps module, making it inaccessible to external consumers. This change adds it to the public re-export list so it can be used by callers of the caps API.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Extract the filesystem-independent shape validation (absolute-vs-relative and `..` traversal) into a new `check_shape` function that runs before any filesystem access, and route the existence and directory checks through `AgentRunner::resolve_workdir` when a harness claims the workspace. This ensures the containment check that matters most cannot be accidentally dropped by a host implementation running agents on a remote filesystem.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The `resolve_working_dir` function and `child_workspace` function were changed from synchronous to asynchronous to support the new async signature of `resolve_node_dir`, which now requires a capability reference for agent workspace resolution. This ensures that working directory resolution properly awaits the underlying filesystem checks and capability validation.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add test coverage for the new `AgentRunner` capability that allows remote hosts to resolve workspace directories without touching the local filesystem. The tests verify that a harness can answer for its own workspace, that refusals fail with the correct reason, that unmanaged answers fall back to local disk, and that path traversal checks still run before the harness is consulted.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
…kdir resolution

Added two new paragraphs to the Node-Catalog page that clarify how the engine handles workdir resolution. The first explains that an expression resolving to null fails the step rather than silently falling back to a default directory, preventing steps from running in an unintended checkout. The second describes how the engine checks the shape of a declared directory but delegates filesystem existence checks to the agent runner, with the shell node taking a separate path through the shell runner and script policy.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Expanded the changelog entry for the workdir resolution change to clarify that a null expression now fails the step, and added a new entry documenting the `AgentRunner::resolve_workdir` and `caps::WorkdirCheck` interface for harnesses that need to answer for their own filesystem.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Your included review limit has been reached.

You’re in a promotional period — use the checkbox below to run this review for free:

  • Run review for free

On-demand reviews are free for the next 28 days. After that, they cost $0.25 per reviewed file.

How can I continue?

Run this review now using the option above, or comment @coderabbitai review --use-credits.

You can also wait for the limit to reset (next review available in 13 minutes), then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 85982df2-6c84-45fd-9524-a5425955f3cf

📥 Commits

Reviewing files that changed from the base of the PR and between 40e4fdb and 74b8cfc.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • src/caps/agent.rs
  • src/caps/agent/runner.rs
  • src/caps/mod.rs
  • src/nodes/integration/agent.rs
  • src/nodes/integration/agent_request.rs
  • src/nodes/integration/sub_workflow/execution.rs
  • src/validate.rs
  • src/validate/node_config.rs
  • src/workdir.rs
  • src/workdir_tests.rs
  • tests/agent_workdir_e2e.rs
  • wiki/Node-Catalog.md

Warning

Your free Security trial is over. An organization admin can activate billing to continue.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

$0.0000 · 0 in / 0 out · 750 embedded · openrouter/openai/text-embedding-3-small

@tinysweeper

tinysweeper Bot commented Aug 23, 2026

Copy link
Copy Markdown

How this change flows

3 changed behaviours across 12 relationships. 5 surrounding behaviours are shown (60 graph nodes walked). 38 further behaviours left out to keep the diagram readable.

flowchart LR
  n0["run_turn_indexed<br/>changed"]:::changed
  n1["child_workspace<br/>changed"]:::changed
  n2["run_child<br/>changed"]:::changed
  n3["map"]:::impacted
  n4["assemble"]:::impacted
  n5["run_graph"]:::impacted
  n6["Result"]:::impacted
  n7["NodeContext"]:::impacted
  n0 -->|calls| n3
  n0 -->|calls| n4
  n0 -->|uses| n6
  n0 -->|uses| n7
  n1 -->|calls| n3
  n2 -->|calls| n1
  n2 -->|calls| n3
  n4 -->|calls| n3
  n4 -->|uses| n6
  n4 -->|uses| n7
  n5 -->|calls| n3
  n5 -->|tests| n3
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge.

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Aug 23, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 74b8cfca91

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +149 to +150
#[tokio::test]
async fn a_cwd_expression_that_resolves_to_null_fails_the_step() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move the new tests into a _tests.rs file

Adding these Rust tests to tests/agent_workdir_e2e.rs leaves them in a filename that does not end in _tests.rs, contrary to the repository's explicit test-file naming requirement; rename or split this integration-test target so the file uses the required suffix.

AGENTS.md reference: AGENTS.md:L6-L7

Useful? React with 👍 / 👎.

@senamakel
senamakel merged commit bfc6e24 into tinyhumansai:main Aug 24, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant